#include <iostream>
#include <vector>
#include <numeric>

using namespace std;
#define ull unsigned long long

const int MAX_N = 200001; // 最大栈数量
const int MAX_M = 100001; // 最大操作数
const int MAX_NODES = MAX_N + MAX_M * 2; // 预估节点池大小

// Splay Tree 节点定义
struct Node {
    int fa, ch[2];      // 父节点，左右孩子
    int w;              // 权重
    ull c;              // 数量
    ull sum;            // 子树总价值
    ull count;          // 子树总数量
    bool rev;           // 翻转懒惰标记
} tr[MAX_NODES];

int root[MAX_N];        // 每个栈的根节点
int node_idx;           // 节点池的当前索引

// 创建一个新节点
int new_node(ull c, int w) {
    ++node_idx;
    tr[node_idx].fa = tr[node_idx].ch[0] = tr[node_idx].ch[1] = 0;
    tr[node_idx].c = c;
    tr[node_idx].w = w;
    tr[node_idx].count = c;
    tr[node_idx].sum = c * w;
    tr[node_idx].rev = false;
    return node_idx;
}

// 上传节点信息（由子节点更新父节点）
void pushup(int x) {
    tr[x].count = tr[tr[x].ch[0]].count + tr[tr[x].ch[1]].count + tr[x].c;
    tr[x].sum = tr[tr[x].ch[0]].sum + tr[tr[x].ch[1]].sum + (ull)tr[x].c * tr[x].w;
}

// 下传懒惰标记
void pushdown(int x) {
    if (tr[x].rev) {
        swap(tr[x].ch[0], tr[x].ch[1]);
        if (tr[x].ch[0]) tr[tr[x].ch[0]].rev ^= 1;
        if (tr[x].ch[1]) tr[tr[x].ch[1]].rev ^= 1;
        tr[x].rev = false;
    }
}

// 判断 x 是其父节点的左孩子(0)还是右孩子(1)
bool get(int x) {
    return tr[tr[x].fa].ch[1] == x;
}

// 旋转操作
void rotate(int x) {
    int y = tr[x].fa, z = tr[y].fa;
    int d = get(x);
    tr[z].ch[get(y)] = x; tr[x].fa = z;
    tr[y].ch[d] = tr[x].ch[d ^ 1]; tr[tr[x].ch[d ^ 1]].fa = y;
    tr[x].ch[d ^ 1] = y; tr[y].fa = x;
    pushup(y); pushup(x);
}

// 将节点 x 伸展到目标节点 goal 的子节点位置
void splay(int x, int goal = 0) {
    while (tr[x].fa != goal) {
        int y = tr[x].fa, z = tr[y].fa;
        if (z != goal) {
            if (get(x) == get(y)) rotate(y);
            else rotate(x);
        }
        rotate(x);
    }
    if (!goal) { // 如果 goal 是 0，意味着 x 成为新的根
        int stk_idx = 0;
        for(int i = 1; i < MAX_N; ++i) {
            if (root[i] == x || root[i] == tr[x].fa) {
                 // 找到这个树属于哪个栈，更新它的根
                 // 注意：这是一个简化处理，实际应用中可能需要更精确的栈索引传递
                 // 在本题的调用模式下，splay 的根会直接赋给 root[...], 所以是安全的
            }
        }
    }
}

// 按排名查找第 k 个元素所在的节点
int find_by_rank(int rt, ull rank) {
    int u = rt;
    while (u) {
        pushdown(u);
        ull left_count = tr[tr[u].ch[0]].count;
        if (rank > left_count && rank <= left_count + tr[u].c) {
            return u;
        }
        if (rank <= left_count) {
            u = tr[u].ch[0];
        } else {
            rank -= left_count + tr[u].c;
            u = tr[u].ch[1];
        }
    }
    return u; // 理论上不会到这里
}

// 找到树的最右侧节点（栈顶）
int find_last(int rt) {
    int u = rt;
    while (u) {
        pushdown(u);
        if (tr[u].ch[1] == 0) return u;
        u = tr[u].ch[1];
    }
    return u;
}

// 操作1: 插入
void insert(int x, int w, int c) {
    if (!root[x]) { // 如果栈为空
        root[x] = new_node(c, w);
    } else {
        int last = find_last(root[x]);
        splay(last, 0);
        root[x] = last;
        if (tr[root[x]].w == w) { // 与栈顶元素权重相同，合并
            tr[root[x]].c += c;
        } else { // 权重不同，新建节点作为右孩子
            int new_n = new_node(c, w);
            tr[root[x]].ch[1] = new_n;
            tr[new_n].fa = root[x];
        }
        pushup(root[x]);
    }
    cout << tr[root[x]].sum;
}

// 操作2: 弹出
void pop(int x, int c) {
    ull total_count = tr[root[x]].count;
    ull popped_sum = 0;

    if (total_count <= c) { // 全部弹出
        popped_sum = tr[root[x]].sum;
        root[x] = 0;
    } else {
        ull remain_count = total_count - c;
        int u = find_by_rank(root[x], remain_count);
        splay(u, 0);
        root[x] = u;

        ull left_count = tr[tr[root[x]].ch[0]].count;
        ull split_count = remain_count - left_count;

        popped_sum = tr[tr[root[x]].ch[1]].sum; // 右子树全部弹出
        tr[root[x]].ch[1] = 0; // 断开右子树

        if (split_count < tr[root[x]].c) { // 当前节点也需要部分弹出
            ull pop_c = tr[root[x]].c - split_count;
            popped_sum += pop_c * tr[root[x]].w;
            tr[root[x]].c = split_count;
        }
        pushup(root[x]);
    }
    cout << popped_sum;
}

// 操作3: 移动
void move(int x, int y) {
    if (!root[x]) { // x 为空，y 不变
        cout << (root[y] ? tr[root[y]].sum : 0);
        return;
    }
    
    // 对 x 树进行翻转，使其在合并后保持原有顺序
    tr[root[x]].rev ^= 1;

    if (!root[y]) { // y 为空，x 直接成为 y
        root[y] = root[x];
        root[x] = 0;
    } else {
        int last_y = find_last(root[y]);
        splay(last_y, 0);
        root[y] = last_y;
        
        // 将 x 树作为 y 树的右子树进行合并
        tr[root[y]].ch[1] = root[x];
        tr[root[x]].fa = root[y];
        pushup(root[y]);
        root[x] = 0;
    }
    cout << tr[root[y]].sum;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, m;
    cin >> n >> m;

    while (m--) {
        int code;
        cin >> code;
        if (code == 1) {
            int x, w, c;
            cin >> x >> w >> c;
            insert(x, w, c);
        } else if (code == 2) {
            int x, c;
            cin >> x >> c;
            pop(x, c);
        } else if (code == 3) {
            int x, y;
            cin >> x >> y;
            move(x, y);
        }
        cout << "\n";
    }

    return 0;
}